<?php
/**
 * This is the base of all classes that need to communicate with sugar via soap. It
 * provides an easy abstract way of managing the entries in the sugar database.
 *
 * @author Niels Janssen
 * @since 2009-04-20
 * @abstract 
 */
abstract class SoapModel
{
	/**
	 * Te sugar module name.
	 *
	 * @var string
	 */
	protected $_sugarModuleName = '';
	
	/**
	 * The columns available to the outside.
	 *
	 * @var array
	 */
	protected $_columns			= array();
	
	/**
	 * The values stored in this object.
	 *
	 * @var array
	 */
	protected $_values			= array();
	
	/**
	 * Whether or not this object is locked and can (or can not) be modified.
	 *
	 * @var boolean
	 */
	private $_frozen			= false;
	
	/**
	 * Whether or not this object has been modified from its original state. If it 
	 * wasn't then any calls to the save method will be ignored, saving performance. 
	 *
	 * @var boolean
	 */
	private $_isModified		= false;
	
	/**
	 * Indicates wheter or not the save method should insert a new entry.
	 *
	 * @var boolean
	 */
	private $_newRecord			= false;
	
	/**
	 * The id sugar has given this record.
	 *
	 * @var string
	 */
	protected $_sugarId 		= '';
	
	/**
	 * The soapbinding
	 *
	 * @var nusoap_client
	 */
	protected static $soapBinding = null;
	
	/**
	 * The fields belonging to this model.
	 *
	 * @var array
	 */
	protected static $fields = array();
	
	/**
	 * Creates a basic SoapModel instance.
	 *
	 * @param array $parameters
	 * @param boolean $newRecord
	 * @param boolean $modified
	 */
	public final function __construct( array $parameters = array(), $newRecord = true, $modified = false )
	{
		foreach( $parameters as $key => $value ) 
		{
			$this->$key = $value;
		}
		
		$this->_isModified	= $modified;
		$this->_newRecord	= $newRecord;
	}
	
	public function update( array $parameters = array(), $save = true )
	{
		foreach( $parameters as $key => $value ) 
		{
			$this->$key = $value;
		}
		
		if( $save )
		{
			$this->save();
		}
	}
	
	/**
	 * Sets the soapbinding to this object so this class can function as expected.
	 *
	 * @param nusoap_client $soapBinding
	 */
	public final static function setSoapBinding( nusoap_client $soapBinding )
	{
		self::$soapBinding = $soapBinding;
	}
	
	/**
	 * Sets te id sugar has given this record.
	 *
	 * @param string $id
	 */
	public final function setSugarId( $id )
	{
		$this->_sugarId = $id;
	}
	
	/**
	 * Returns te id sugar has given this record.
	 *
	 * @return string
	 */
	public final function getSugarId( )
	{
		return $this->_sugarId;
	}
	
	/**
	 * Returns the name of the sugar counterpart.
	 *
	 * @return string
	 */
	public final function getSugarModuleName()
	{
		return $this->_sugarModuleName;
	}
	
	/**
	 * Returns the columns that are available.
	 *
	 * @return array
	 */
	public final function getColumns()
	{
		return $this->_columns;
	}
	
	/**
	 * Returns the attributes of the current object.
	 *
	 * @return array
	 */
	public final function getAttributes()
	{
		return $this->_values;
	}
	
	/**
	 * Finds and returns the field.
	 *
	 * @param string $class
	 * @param string $fieldName
	 * @return SoapField
	 */
	public static function getField( $class, $fieldName )
	{
		$object = new $class( );
		
		if( empty( self::$fields[ $object->getSugarModuleName() ] ) )
		{
			self::buildFieldCache( $object->getSugarModuleName() );
		}
		
		if( $fieldName == '*' )
		{
			return self::$fields[ $object->getSugarModuleName() ];
		}
		
		if( isset( self::$fields[ $object->getSugarModuleName() ][ $fieldName ] ) )
		{
			return self::$fields[ $object->getSugarModuleName() ][ $fieldName ];
		}
		
		throw new Exception('Field "' . $fieldName . '" in "' . $object->getSugarModuleName() . '" could not be found.');
	}
	
	public static function buildFieldCache( $moduleName, $session_id = null )
	{
		$fieldResult = self::$soapBinding->call( 'get_module_fields', array(
			'session' => !isset( $session_id ) ? User::getSessionId() : $session_id,
			'module_name' => $moduleName
		) );
		
		self::validateResult( $fieldResult );
		
		self::$fields[ $moduleName ] = SoapField::createFromArray( $fieldResult['module_fields'] );
	}
	
	/**
	 * Provides an abstracted interface to sugarsoaps (complicated) way to get one
	 * or more entries from its database.
	 *
	 * @param string $class
	 * @param string $id
	 * @param array $options
	 * @return SoapModel
	 */
	public static function find( $class, $id, $options )
	{
		$object = new $class(  );
		
		/*
		 * Check if we have a field cache, if not retrieve the fields for this model.
		 */
		if( empty( self::$fields[ $object->getSugarModuleName() ] ) )
		{
			self::buildFieldCache( $object->getSugarModuleName(), !isset( $options['session_id'] ) ? User::getSessionId() : $options['session_id'] );
		}
		
		switch( $id )
		{
			case 'first':
				$options['max_results'] = 1;
			case 'all':
				$result = self::$soapBinding->call( 'get_entry_list', array(
					// Session id [string]
					'session' => !isset( $options['session_id'] ) ? User::getSessionId() : $options['session_id'],
					
					// Module name [string]
					'module_name' => $object->getSugarModuleName(),
					
					// Query string [string]
					'query' => isset( $options['query'] ) ? $options['query'] : '',
					
					// Order by field [string]
					'order_by' => isset( $options['order_by'] ) ? $options['order_by'] : '',
					
					// Numerical offset [int]
					'offset' => isset( $options['offset'] ) ? $options['offset'] : 0,
					
					// The selected fields [array]
					'select_fields' => array_values( $object->getColumns() ),
					
					// Max results [int]
					'max_results' => isset( $options['max_results'] ) ? $options['max_results'] : 30,
					
					// Deleted [boolean]
					'deleted' => isset( $options['deleted'] ) ? $options['deleted'] : false					
				) );				
				break;
				
			default:
				$result = self::$soapBinding->call( 'get_entry', array(
					// Session id
					'session' => !isset( $options['session_id'] ) ? User::getSessionId() : $options['session_id'],
					
					// Module name
					'module_name' => $object->getSugarModuleName(),
					
					// The row id
					'id' => $id,
					
					// The selected fields
					' select_fields' => array_values( $object->getColumns() )
				) );
				break;
		}

		self::validateResult( $result );
		
		switch( $id )
		{
			case 'all':
				$baseObjects = array();
				
				$metadata = new stdClass();
				$metadata->result_count = $result['result_count'];
				$metadata->next_offset = $result['next_offset'];
								
				foreach( $result['entry_list'] as $entry )
				{
					$object = self::prepareObject( $class, $entry );
					$baseObjects[ $object->getSugarId() ] = $object;
				}
				
				$result = new SoapResultSet( $metadata, $baseObjects );
				break;
				
			default:
				$result = self::prepareObject( $class, array_pop( $result['entry_list'] ) );
				break;
		}
		
		return $id == 'all' ? $baseObjects : $result;
		
	}
	
	/**
	 * Preps an object for use. Loads the object with its properties.
	 *
	 * @param SoapModel $class
	 * @param stdClass $entry
	 * @return SoapModel
	 */
	private final static function prepareObject( $class, $entry )
	{
		$parameters = array();
		
		foreach( $entry['name_value_list'] as $row )
		{
			$parameters[ $row['name'] ] = $row['value'];
		}

		$object = new $class( $parameters, false );
		$object->setSugarId( $entry['id'] );
		
		return $object;
	}
	
	/**
	 * Validates the result of a sugarsoap request, throws an exception
	 * if the result contains errors.
	 *
	 * @param stdClass $result
	 */
	protected final static function validateResult( $result )
	{
		if( $result == FALSE )
		{
			throw new SoapModelException( 'Request could not be completed, are you connected to the internet?' );	
		}
		
		if( isset( $result['error'] ) && is_array( $result['error'] ) && !( $result['error']['number'] == 0 && $result['error']['name'] == "No Error" ) )
		{
			if( !empty( $result['error']['name'] ) )
			{
				throw new SoapModelResultException( 'An error occurred while processing the result', $result['error'] );
			}
		}
	}
	
	/**
	 * Provides acces to get a property directly.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public final function __get( $key )
	{
		if( in_array( Inflector::underscore( $key ), array_keys( $this->_values ) ) )
		{
			return $this->_values[ Inflector::underscore( $key ) ];
		}
		else
			throw new SoapModelException( "Attribute called '$key' does not exist", SoapModelException::AttributeNotFound );
	}
	
	/**
	 * Provides access to set a property directly.
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public final function __set( $key, $value )
	{
		if( $this->_frozen )
			throw new SoapModelException("Can not change property '$key' because the object is locked.", SoapModelException::ObjectLocked);
		$this->_values[ Inflector::underscore( $key ) ] = $value;
		$this->_isModified = true;
	}
	
	/**
	 * Saves this record to the SugarCRM mothership.
	 */
	public final function save()
	{
		if( $this->_frozen )
			throw new SoapModelException('Can not save because the object is locked.', SoapModelException::ObjectLocked);
		
		if( method_exists( $this, 'beforeSave' ) )
		{
			$this->beforeSave();
		}
			
		/*
		 * Convert the internal values array to the required name, value format. 
		 */
		$nameValueList = array();
		
		foreach( $this->_values as $name => $value )
		{
			$nameValueList[] = array( 'name' => $name, 'value' => $value );
		}
		
		$result = self::$soapBinding->call( 'set_entry', array(
			'session' => User::getSessionId(),
			'module_name' => $this->getSugarModuleName(),
			'name_value_list' => $nameValueList
		) );
		
		self::validateResult( $result );
		
		if( $this->_newRecord )
		{
			$this->_sugarId = $result['id'];
			$this->_newRecord = false;
		}
	}
	
	/**
	 * Marks the current record as deleted.
	 *
	 */
	public final function delete()
	{
		if( $this->_frozen )
			throw new SoapModelException('Can not delete because the object is locked.', SoapModelException::ObjectLocked);
		
		$this->deleted = 1;
		$this->save();
	}
	
	/**
	 * Handles virtual methods. Provides access to propeties through the natural
	 * get/set method interfaces.
	 *
	 * @param string $name
	 * @param array $arguments
	 * @return mixed
	 */
	public final function __call($name, $arguments) {
		switch (strtolower ( substr ( $name, 0, 3 ) ) ) {
			case 'get' :
				if (in_array ( Inflector::underscore ( substr ( $name, 3 ) ), array_keys( $this->_values ) ))
				{
					$name = Inflector::underscore ( substr ( $name, 3 ) );
					return $this->$name;
				}
				else
					throw new SoapModelException ( "attribute called '$name' doesn't exist", SoapModelException::AttributeNotFound );
				break;

			case 'set' :
				if ($this->frozen)
					throw new SoapModelException ( "Can not update $name as object is frozen.", SoapModelException::ObjectFrozen );
				if (in_array ( Inflector::underscore ( substr ( $name, 3 ) ), array_keys( $this->_values ) ))
				{
					$name = Inflector::underscore ( substr ( $name, 3 ) );
					$this->$name = array_shift ( $arguments );
					$this->is_modified = true;
					return true;
				}
				else
					throw new SoapModelException ( "attribute called '$name' doesn't exist", SoapModelException::AttributeNotFound );
				break;

			default :
				throw new SoapModelException ( "method called '$name' doesn't exist", SoapModelException::AttributeNotFound );
				break;
		}
	}
	
	/**
	 * Lock an object to prevent it from being saved/updated.
	 *
	 */
	public final function lock()
	{
		$this->_frozen = true;
	}
	
	/**
	 * Unlocks the object so it can be saved/updated again.
	 *
	 */
	public final function unlock()
	{
		$this->_frozen = false;
	}
	
	/**
	 * Links the current module to another module.
	 *
	 * @param SoapModel $model
	 */
	public final function link( SoapModel $model )
	{
		$result = self::$soapBinding->call( 'set_relationship', array(
			'session' => User::getSessionId(),
			'set_relationship_value' => array(
				'module1' => $this->getSugarModuleName(),
				'module1_id' => $this->getSugarId(),
				'module2' => $model->getSugarModuleName(),
				'module2_id' => $model->getSugarId()
			)
		) );
		
		self::validateResult( $result );
	}
}

class SoapModelResultException
	extends Exception
{
	
	protected $errorObject = null;
	
	public function __construct( $message, $errorObject )
	{
		parent::__construct( $message, 0x0A );
		$this->errorObject = $errorObject;
	}
	
	public function __toString()
	{
		return "SoapModelResultException: " . $this->getMessage() . "\nError: \n" . print_r( $this->errorObject, true ) . "\n" . $this->getTraceAsString();;
	}
}

class SoapModelException
	extends Exception 
{
	const AttributeNotFound = 0x01;
	const ObjectLocked = 0x02;
}
